iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
Software Development

程式碼氣味到重構之路 Code Smells to Refactorings系列 第 8

Primitive Obsession > Refactoring 如何重構基本型別偏執

  • 分享至 

  • xImage
  •  

本篇文章會介紹如何重構基本型別偏執(Primitive Obsession),根據對照表上一共有十四種重構手法可以對應。部分重構技巧在之前的系列文章中已經提及。

  1. Extract Class
  2. Replace Data Value with Object
  3. Encapsulate Composite with Builder
  4. Introduce Parameter Object
  5. Preserve Whole Object
  6. Move Embellishment to Decorator
  7. Replace Conditional Logic with Strategy
  8. Replace Implicit Language with Interpreter
  9. Replace State-Altering Conditionals with State
  10. Replace Implicit Tree with Composite
  11. Replace Type Code with Class
  12. Replace Type Code with State/Strategy
  13. Replace Type Code with Subclasses
  14. Replace Array With Object

當我整理完後,我認為其中有三種重構手法(以刪除線標示)實際上與基本型別偏執這個氣味的關係不大。當然這只是我個人的看法,為了尊重原始對照表,我還是會列出並說明。


Extract Class 抽出類別

Extract Class(抽出類別)在前面介紹「Large Class > Refactoring 如何重構大類別 」一文中已經提及,重複部分不贅述,這裡補充一些新的個人觀點。

如同昨日所說,大部分可以用來消除「Primitive Obsession(基本型別偏執)」氣味的重構技巧,本質上都可以算是一種抽出類別的變形,差異只有抽出後類別的不同,例如抽出邏輯後建立新的狀態類別(State Class)來取代的技巧,稱之為「Replace Type Code with State/Strategy」。

換言之,我們也可以反過來比較兩種氣味:「基本型別偏執」或許也可以視為一種特定的「大類別(Large Class)」,只是肥大的部分侷限在資料型別應用之上。這也間接說明了為什麼改善這個氣味的重構手法,都聚焦在建立新的類別來取代基本型別,來達到讓原本的類別能夠更小更簡潔的瘦身效果。

Replace Data Value with Object 轉換資料為物件

這個重構技巧一樣在「Large Class > Refactoring 如何重構大類別 」中提過,暫時沒有更多補充。

Encapsulate Composite with Builder

要了解這個重構技巧,首先需要認識兩種設計模式:Composite Pattern 與 Builder Pattern。

Composite Pattern(組合模式)是一種結構性設計模式。這個模式會將物件組合成樹狀結構,這些結構內的子元素有如其他物件一樣共同工作。這種設計模式的限制是,核心物件與其元素必須要能夠符合「樹狀結構」,否則反而會讓邏輯更混亂複雜。

其中一個例子是,當你的核心物件存在如「步驟」或「狀態」這樣的邏輯,用樹狀結構去理解反倒容易產生誤解。這個時候我們可以考慮使用「Builder Pattern(建造者模式)」來進行重構。這個模式的優點是可以將物件的「建構」與「表示」邏輯拆解為不同元件,每一個步驟都可以封裝在單一類別或物件內,然後實作出複雜的商業邏輯。

建造者模式的優點是建造物件時考慮不同步驟上更為靈活,可以重複使用相同的核心類別建造出不同的物件,同時隱藏具體實作細節在每一個小步驟類別之中。缺點是會產生大量的小類別,但考慮到我們針對氣味的重構目標,這也不一定算得上是缺點。

設計模式本身有很多內容可以深入談,也有不少鐵人賽專門介紹設計模式,請恕我稍微藏拙。

Introduce Parameter Object 導入參數物件

這個重構技巧我在「Long Method > Refactoring 如何重構長方法」曾經介紹過,屬於 Simplifying Method Calls 分類之下的一種重構手法。

當我們的參數中存在多個由基本型別所組成的物件,例如一個包括開始與結束日期區間的參數中,年、月、日分別由六個整數型別來呈現時,我們可以考慮建立一個包括兩個DateTime的區間類別(Rage Class)來進行重構。

Preserve Whole Object

有關這個重構技巧的說明,一樣在「Long Method > Refactoring 如何重構長方法」曾經介紹過。雖然說根據對照表所示,這個重構技巧可以用來消除基本型別偏執的氣味,坦白說我不同意,並且在我後面會製作的版本中,會將這個重構技巧移除與基本型別偏執之間的對應關係。

Preserve Whole Object 是用物件本身來取代透過物件取得的數值作為參數,但我並不認為這與基本型別有關。

Move Embellishment to Decorator

又進入到說文解字的環節,首先來簡單介紹「裝飾者設計模式(Decorator Design Pattern)」。

傳統上在物件導向程式語言當中,當我們要在多個類別當中分享共同邏輯時,首先會想到透過「繼承」,也就是用父類別的方式來擴充共享一些功能。但是「繼承」有許多缺點,首先繼承是靜態的,我們無法在運行中去修改物件的繼承關係;再者,多數物件導向語言中,一個類別只能繼承自單一一個父類別;最後是,當父類別被越來越多的子類別所共享時,很可能每個子類別實際上都只需要父類別的一小部分,而額外多餘的父類別透過繼承反而可能讓子類別產生非預期的副作用效果。

為了改善上述的「繼承」問題,裝飾者設計模式能夠更彈性的動態依照需求在不同時機擴充與共享功能,來避免子類別與父類別耦合太高的問題。比起父類別繼承的方式,每一個「裝飾者」也更能符合單一職責原則,把核心邏輯、結構與額外的「裝飾」部分抽離出來。

Replace Conditional Logic with Strategy

這個類別又可稱為「Replace Conditional with Polymorphism」,將冗長的條件判斷式以「多形(Polymorphism)」來替代,創造出許多小類別。在關於「Long Method > Refactoring 如何重構長方法」已經有過介紹。

坦白說我個人對於 Conditional Logic 是否算是一種基本型別也感到疑惑。這個重構技巧主要是用來簡化複雜的條件判斷子句,固然條件判斷句中的充滿著布林值(Boolean)是一種基本型別,但要說這個手法能夠用來消除「基本型別偏執(Primitive Obsession)」,我認為尚屬牽強。

暫時先不拿掉,但是會放在刪除的候補清單中。

Replace Implicit Language with Interpreter

這個重構技巧使用「Interpreter Design Pattern(解譯器模式)」來消除程式碼中的「Implicit Language(隱式語言)」。在「Large Class > Refactoring 如何重構大類別 」中已經提過。

在我的觀點中,Implicit Language 並不能用來消除基本型別偏執(Primitive Obsession),所以在我的版本中會移除。

Replace State-Altering Conditionals with State

在「Large Class > Refactoring 如何重構大類別 」中提過。我也不認為符合基本型別偏執,理由同上。

Replace Implicit Tree with Composite

在上面介紹「Encapsulate Composite with Builder」時有提到 Composite Pattern(組合模式)是一種結構性設計模式。這個重構技巧便是把隱晦不明的樹狀結構,用 Composite Pattern 來重構。

Replace Type Code with Class

這個重構技巧又可稱之為「Replace Data Value with Object」或是「Replace Type Code with Class」,在「Large Class > Refactoring 如何重構大類別 」中介紹過。本質上與 Extract Class 相同,只是抽出 Type Code,把基本型別抽出為新的方法。

Type Code(型別碼)是指某一個資料欄位中存在許可的值列表,而不僅僅是任意基本型別。每一個合法的值都可以對應到特殊的商業邏輯,如郵遞區號、機場代號、國家代碼等。例如一個機票上的機場代碼欄位出現字串TPE,代表的意義是桃園國際機場,並不是任意三個大寫英文字母都是合法的值。

將這些包含特定驗證規則的邏輯集中在相同的類別中,不僅可以提高內聚,也讓這些 Type Code 規則的可重用性提高,減少了重複程式碼片段散落的風險。

Replace Type Code with Subclasses

延續上面機場代碼的例子,抽換為類別時,每個機場都是機場類別的實體,換言之每個機場的類別是相同的,共享相同的方法;在Subclass的狀況,每個機場都有自己專屬的類別,繼承一個共通的機場類別(可能包含塔台、候機室、跑道等功能)。最主要的差異是,當每個機場都有各自的類別是,每個機場可以有自己專屬不同的方法。

例如水上飛機的機場繼承機場共通父類別,但是將跑道功能覆寫為水道,供水上飛機滑行使用,但是塔台、候機室等功能依然繼承自父類別維持不變。

Replace Type Code with State/Strategy

當我們想要抽出 Type Code 中的基本型別但是又不想要影響到其他行為時,可以考慮用 Strategy Object 而不是類別來替代。

兩者的差異為,類別聚焦在封裝特定型別(Type Code)的行為與屬性;而狀態、策略物件(State/Strategy Object)則著重在邏輯上。至於狀態與策略的選擇,如果我們的目標是將控制選擇的演算法抽離,使用策略物件;如果些狀態條件判斷包括物件的資料與行為本身的不同,選擇狀態物件。

Replace Array With Object

這個重構技巧是「Replace Data Value with Object」的其中一種特例,只是將 Data Value 限定在 Array。

在多數物件導向程式語言中,Array(陣列)都是一種常見也常用的基本型別,因此也容易遭到誤用。比如說當我們有一組不同意義的資料儲存在同一個陣列內,可以考慮將這個陣列用包括商業邏輯的物件來取代。

來看看下面的範例程式碼:

String[] user = new String[2];
user[0] = "Bater";
user[1] = "37";

上面的程式碼可說是相當不清楚,而且欄位的意義依賴順序。順序是很不可靠的內在邏輯,user[0]代表姓名欄位可以說是一種Magical Number。我們可以做以下重構:

User user = new User();
user.setName("Bater");
user.setAge("37");

比較兩者:user.setName()的可讀性大勝user[0]=,也不需擔心無法按照順序給資料得情形。

Reference

https://www.industriallogic.com/xp/refactoring/compositeWithBulder.html
https://refactoring.guru/design-patterns/composite
https://refactoring.guru/design-patterns/builder
https://www.industriallogic.com/xp/refactoring/embellishmentToDecorator.html
https://refactoring.guru/design-patterns/decorator
https://www.industriallogic.com/xp/refactoring/conditionalWithStrategy.html
https://www.informit.com/articles/article.aspx?p=1398607&seqNum=5
https://refactoring.guru/replace-type-code-with-state-strategy
https://refactoring.guru/replace-array-with-object


上一篇
Bloaters > Primitive Obsession 基本型別偏執
下一篇
Bloaters > Long Parameter List 過長參數列與如何重構
系列文
程式碼氣味到重構之路 Code Smells to Refactorings37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言